﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Ue.Core;
using Ue.Core.FileSystems;
using Ue.Eagle.Runtime.FileSystems;
using Ue.Eagle.Runtime.FileSystems.Dependencies;
using Ue.Eagle.Runtime.Loaders;
using Ue.Eagle.Runtime.Models;
using Ue.Eagle.Runtime.Utility;

namespace Ue.Eagle.Runtime
{
    public class PluginLoaderCoordinator : IPluginLoaderCoordinator
    {
        private readonly IDependenciesFolder dependenciesFolder;
        private readonly IPluginDependenciesManager pluginDependenciesManager;
        private readonly IPluginManager pluginManager;
        private readonly IRootFolder rootFolder;
        private readonly IEnumerable<IPluginLoader> loaders;
        private readonly IHostEnvironment hostEnvironment;
        private readonly IBuildManager buildManager;

        public PluginLoaderCoordinator(
            IDependenciesFolder dependenciesFolder,
            IPluginDependenciesManager pluginDependenciesManager,
            IPluginManager pluginManager,
            IRootFolder rootFolder,
            IEnumerable<IPluginLoader> loaders,
            IHostEnvironment hostEnvironment,
            IBuildManager buildManager)
        {
            this.dependenciesFolder = dependenciesFolder;
            this.pluginDependenciesManager = pluginDependenciesManager;
            this.pluginManager = pluginManager;
            this.rootFolder = rootFolder;
            this.loaders = loaders;
            this.hostEnvironment = hostEnvironment;
            this.buildManager = buildManager;
        }

        public void SetupPlugins()
        {
            var context = this.CreatePluginLoadingContent();

            foreach (var dependency in context.DeletedDependencies)
            {
                foreach (var loader in this.loaders)
                {
                    if (dependency.LoaderName == loader.Name)
                    {
                        loader.PluginRemoved(context, dependency);
                    }
                }
            }

            foreach (var plugin in context.AvailablePlugins)
            {
                this.ProcessPlugin(context, plugin);
            }

            // Execute all the work need by "ctx"
            this.ProcessContextCommands(context);

            // And finally save the new entries in the dependencies folder
            this.dependenciesFolder.StoreDescriptors(context.NewDependencies);
            this.pluginDependenciesManager.StoreDependencies(context.NewDependencies, desc => this.GetPluginHash(context, desc));


            // Very last step: Notify the host environment to restart the AppDomain if needed
            if (context.RestartAppDomain)
            {
                this.hostEnvironment.RestartAppDomain();
            }
        }

        private string GetPluginHash(PluginLoadingContext context, DependencyDescriptor dependencyDescriptor)
        {
            var hash = new Hash();
            hash.AddStringInvariant(dependencyDescriptor.Name);

            foreach (var virtualpathDependency in context.ProcessedPlugins[dependencyDescriptor.Name].VirtualPathDependencies)
            {
                hash.AddDateTime(GetVirtualPathModificationTimeUtc(context.VirtualPathModficationDates, virtualpathDependency));
            }

            foreach (var reference in dependencyDescriptor.References)
            {
                hash.AddStringInvariant(reference.Name);
                hash.AddString(reference.LoaderName);
                hash.AddDateTime(GetVirtualPathModificationTimeUtc(context.VirtualPathModficationDates, reference.VirtualPath));
            }

            return hash.Value;
        }

        private void ProcessPlugin(PluginLoadingContext context, PluginDescriptor plugin)
        {
            var pluginProbes = context.AvailablePluginsProbes.ContainsKey(plugin.Id)
                ? context.AvailablePluginsProbes[plugin.Id]
                : Enumerable.Empty<PluginProbeEntry>();

            pluginProbes = pluginProbes.ToArray();

            //当前插件依赖的插件
            var moduleReferences =context
                .AvailablePlugins
                .Where(e =>
                        context.ReferencesByModule.ContainsKey(plugin.Id) &&
                        context.ReferencesByModule[plugin.Id].Any(r => StringComparer.OrdinalIgnoreCase.Equals(e.Id, r.Name)))
                .ToList();

            var processedModuleReferences = moduleReferences
                .Where(e => context.ProcessedPlugins.ContainsKey(e.Id))
                .Select(e => context.ProcessedPlugins[e.Id])
                .ToList();

            var activatedPlugin = pluginProbes.FirstOrDefault(
                e => e.Loader.IsCompatibleWithModuleReferences(plugin, processedModuleReferences)
            );

            var previousDependency = context.PreviousDependencies.FirstOrDefault(
                d => StringComparer.OrdinalIgnoreCase.Equals(d.Name, plugin.Id)
            );

            var references = this.ProcessPluginReferences(context, activatedPlugin);

            foreach (var loader in this.loaders)
            {
                if (activatedPlugin != null && activatedPlugin.Loader.Name == loader.Name)
                {
                    loader.PluginActivated(context, plugin);
                }
                else if (previousDependency != null && previousDependency.LoaderName == loader.Name)
                {
                    loader.PluginDeactivated(context, plugin);
                }
            }

            if (activatedPlugin != null)
            {
                context.NewDependencies.Add(new DependencyDescriptor
                {
                    Name = plugin.Id,
                    LoaderName = activatedPlugin.Loader.Name,
                    VirtualPath = activatedPlugin.VirtualPath,
                    References = references
                });
            }

            context.ProcessedPlugins.Add(plugin.Id, activatedPlugin);
        }

        private PluginLoadingContext CreatePluginLoadingContent()
        {
            var availablePlugins = this.pluginManager
                .AvailablePlugins()
                .OrderBy(p => p.Id)
                .ToList();

            //检测重复插件
            var duplicates = availablePlugins
                .GroupBy(p => p.Id)
                .Where(g => g.Count() >= 2)
                .ToList();

            if (duplicates.Any())
            {
                var sb = new StringBuilder();
                sb.Append("在插件目录中，存在名称相同的插件。\r\n");
                foreach (var dup in duplicates)
                {
                    sb.Append(string.Format("插件“{0}”存在于以下位置：{1} \r\n", string.Join(", ", dup.Key, dup.Select(d => d.Location))));
                }
                sb.Append("可以通过删除插件，或更改插件名称，解决当前问题。");
                throw new Exception(sb.ToString());
            }

            //前一次运行插件描述列表
            var previousDependencies = this.dependenciesFolder
                .LoadDescriptors()
                .ToList();

            //所有插件的VirtualPathDependencies文件的最后修改时间
            //Key：VirtualPath
            //Value:FileLastWriteTimeUtc
            var virtualPathModficationDates = new ConcurrentDictionary<string, DateTime>(StringComparer.OrdinalIgnoreCase);

            //将每个插件都分别以加载器集合中的加载器加载。
            //同一个插件，可能会以不同的加载器加载成功，所以分组显示。
            var availablePluginsProbes1 = availablePlugins
                .Select(plugin => this.loaders
                    .Select(loader => loader.Probe(plugin))
                    .Where(entry => entry != null)
                    .ToArray())
                .SelectMany(entries => entries)
                .GroupBy(entry => entry.Descriptor.Id);

            //插件Probes，根据插件的优先级进行排序
            //Key：PluginId
            //Value：IEnumaerable<PluginProbeEntry>
            var availablePluginsProbes = availablePluginsProbes1
                .Select(g => 
                    new { 
                        Id = g.Key, 
                        Entries = this.SortPluginProbeEntries(g, virtualPathModficationDates) 
                    }
                )
                .ToDictionary(g => g.Id, g => g.Entries, StringComparer.OrdinalIgnoreCase);

            //删除的插件
            var deletedDependencies = previousDependencies
                .Where(e => !availablePlugins.Any(e2 => StringComparer.OrdinalIgnoreCase.Equals(e2.Id, e.Name)))
                .ToList();

            var references = availablePlugins
                .Select(plugin => loaders
                    .SelectMany(loader => loader.ProbeReferences(plugin))
                    .ToList())
                .SelectMany(entries => entries)
                .ToList();

            var referencesByModule = references
                .GroupBy(entry => entry.Descriptor.Id, StringComparer.OrdinalIgnoreCase)
                .ToDictionary(g => g.Key, g => g.AsEnumerable(), StringComparer.OrdinalIgnoreCase);

            var referencesByName = references
                .GroupBy(reference => reference.Name, StringComparer.OrdinalIgnoreCase)
                .ToDictionary(g => g.Key, g => g.AsEnumerable(), StringComparer.OrdinalIgnoreCase);

            var sortedAvailablePlugins =
                availablePlugins.OrderByDependenciesAndPriorities(
                    (item, dep) => referencesByModule.ContainsKey(item.Id) &&
                                   referencesByModule[item.Id].Any(r => StringComparer.OrdinalIgnoreCase.Equals(dep.Id, r.Name)),
                    item => 0)
                    .ToList();

            return new PluginLoadingContext
            {
                AvailablePlugins = sortedAvailablePlugins,
                PreviousDependencies = previousDependencies,
                DeletedDependencies = deletedDependencies,
                AvailablePluginsProbes = availablePluginsProbes,
                ReferencesByName = referencesByName,
                ReferencesByModule = referencesByModule,
                VirtualPathModficationDates = virtualPathModficationDates,
            };
        }

        private IEnumerable<PluginProbeEntry> SortPluginProbeEntries(IEnumerable<PluginProbeEntry> entries, ConcurrentDictionary<string, DateTime> virtualPathModficationDates)
        {
            // 根据优先级进行分组，并根据优先级排序
            var groupByPriority = entries
                .GroupBy(entry => entry.Priority)
                .OrderByDescending(g => g.Key);

            // 选择优先级别最高的一组，组中至少必须有一个项目
            var firstNonEmptyGroup = groupByPriority.FirstOrDefault(g => g.Any()) ?? Enumerable.Empty<PluginProbeEntry>();

            // 如果分组中小于一个项目，则没必要进行排序
            if (firstNonEmptyGroup.Count() <= 1)
                return firstNonEmptyGroup;

            // 通过最后修改日期/加载顺序排序
            return firstNonEmptyGroup
                .OrderByDescending(probe => GetVirtualPathDepedenciesModificationTimeUtc(virtualPathModficationDates, probe))
                .ThenBy(probe => probe.Loader.Priority)
                .ToList();
        }

        private DateTime GetVirtualPathDepedenciesModificationTimeUtc(ConcurrentDictionary<string, DateTime> virtualPathDependencies, PluginProbeEntry probe)
        {
            if (!probe.VirtualPathDependencies.Any())
                return DateTime.MinValue;

            return probe.VirtualPathDependencies.Max(path => GetVirtualPathModificationTimeUtc(virtualPathDependencies, path));
        }

        private DateTime GetVirtualPathModificationTimeUtc(ConcurrentDictionary<string, DateTime> virtualPathDependencies, string path)
        {
            return virtualPathDependencies.GetOrAdd(path, p => this.rootFolder.GetFileLastWriteTimeUtc(p));
        }

        IEnumerable<DependencyReferenceDescriptor> ProcessPluginReferences(PluginLoadingContext context, PluginProbeEntry activatedExtension)
        {
            if (activatedExtension == null)
                return Enumerable.Empty<DependencyReferenceDescriptor>();

            var referenceNames = (context.ReferencesByModule.ContainsKey(activatedExtension.Descriptor.Id)
                ? context.ReferencesByModule[activatedExtension.Descriptor.Id] 
                : Enumerable.Empty<PluginReferenceProbeEntry>())
                .Select(r => r.Name)
                .Distinct(StringComparer.OrdinalIgnoreCase);

            var referencesDecriptors = new List<DependencyReferenceDescriptor>();
            foreach (var referenceName in referenceNames)
            {
                ProcessPluginReference(context, activatedExtension, referenceName, referencesDecriptors);
            }

            return referencesDecriptors;
        }

        private void ProcessPluginReference(PluginLoadingContext context,
            PluginProbeEntry activatedPlugin,
            string referenceName,
            IList<DependencyReferenceDescriptor> activatedReferences)
        {

            // If the reference is an extension has been processed already, use the same loader as 
            // that extension, since a given extension should be loaded with a unique loader for the 
            // whole application
            var bestPluginReference = context.ProcessedPlugins.ContainsKey(referenceName)
                ? context.ProcessedPlugins[referenceName]
                : null;

            // Activated the extension reference
            if (bestPluginReference != null)
            {
                activatedReferences.Add(new DependencyReferenceDescriptor
                {
                    LoaderName = bestPluginReference.Loader.Name,
                    Name = referenceName,
                    VirtualPath = bestPluginReference.VirtualPath
                });

                return;
            }

            // Skip references from "~/bin"
            if (this.buildManager.HasReferencedAssembly(referenceName))
                return;

            // Binary references
            var references = context.ReferencesByName.ContainsKey(referenceName)
                ? context.ReferencesByName[referenceName]
                : Enumerable.Empty<PluginReferenceProbeEntry>();

            var bestBinaryReference = references
                .Where(entry => !string.IsNullOrEmpty(entry.VirtualPath))
                .Select(entry => new { Entry = entry, LastWriteTimeUtc = this.rootFolder.GetFileLastWriteTimeUtc(entry.VirtualPath) })
                .OrderBy(e => e.LastWriteTimeUtc)
                .ThenBy(e => e.Entry.Name)
                .FirstOrDefault();

            // Activate the binary ref
            if (bestBinaryReference != null)
            {
                if (!context.ProcessedReferences.ContainsKey(bestBinaryReference.Entry.Name))
                {
                    context.ProcessedReferences.Add(bestBinaryReference.Entry.Name, bestBinaryReference.Entry);
                    bestBinaryReference.Entry.Loader.ReferenceActivated(context, bestBinaryReference.Entry);
                }
                activatedReferences.Add(new DependencyReferenceDescriptor
                {
                    LoaderName = bestBinaryReference.Entry.Loader.Name,
                    Name = bestBinaryReference.Entry.Name,
                    VirtualPath = bestBinaryReference.Entry.VirtualPath
                });
                return;
            }
        }

        private void ProcessContextCommands(PluginLoadingContext ctx)
        {
            foreach (var action in ctx.DeleteActions)
            {
                action();
            }

            foreach (var action in ctx.CopyActions)
            {
                action();
            }
        }
    }
}
